msg_tool\scripts\cat_system/
cstl.rs

1//! CatSystem2 Scene I18N File (.cstl)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use anyhow::Result;
7use std::collections::BTreeMap;
8use std::io::{Read, Write};
9
10#[derive(Debug)]
11/// Builder for CatSystem2 Scene I18N Script files.
12pub struct CstlScriptBuilder {}
13
14impl CstlScriptBuilder {
15    /// Creates a new instance of `CstlScriptBuilder`.
16    pub fn new() -> Self {
17        CstlScriptBuilder {}
18    }
19}
20
21impl ScriptBuilder for CstlScriptBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Utf8
24    }
25
26    fn build_script(
27        &self,
28        buf: Vec<u8>,
29        _filename: &str,
30        encoding: Encoding,
31        _archive_encoding: Encoding,
32        config: &ExtraConfig,
33        _archive: Option<&Box<dyn Script>>,
34    ) -> Result<Box<dyn Script>> {
35        Ok(Box::new(CstlScript::new(buf, encoding, config)?))
36    }
37
38    fn extensions(&self) -> &'static [&'static str] {
39        &["cstl"]
40    }
41
42    fn script_type(&self) -> &'static ScriptType {
43        &ScriptType::CatSystemCstl
44    }
45
46    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
47        if buf_len >= 4 && buf.starts_with(b"CSTL") {
48            return Some(15);
49        }
50        None
51    }
52
53    fn can_create_file(&self) -> bool {
54        true
55    }
56
57    fn create_file<'a>(
58        &'a self,
59        filename: &'a str,
60        writer: Box<dyn WriteSeek + 'a>,
61        encoding: Encoding,
62        file_encoding: Encoding,
63        config: &ExtraConfig,
64    ) -> Result<()> {
65        create_file(
66            filename,
67            writer,
68            encoding,
69            file_encoding,
70            config.custom_yaml,
71        )
72    }
73}
74
75/// Create a new CSTL file.
76///
77/// * `custom_filename` - The path of input file.
78/// * `file` - The writer to write the CSTL file to.
79/// * `encoding` - The encoding of the CSTL file.
80/// * `output_encoding` - The encoding to use for the input file.
81/// * `yaml` - Whether to use YAML format.
82pub fn create_file<T: Write>(
83    custom_filename: &str,
84    mut file: T,
85    encoding: Encoding,
86    output_encoding: Encoding,
87    yaml: bool,
88) -> Result<()> {
89    let input = crate::utils::files::read_file(custom_filename)?;
90    let s = decode_to_string(output_encoding, &input, true)?;
91    let data: BTreeMap<String, Vec<Message>> = if yaml {
92        serde_yaml_ng::from_str(&s)?
93    } else {
94        serde_json::from_str(&s)?
95    };
96    let count = data
97        .first_key_value()
98        .ok_or(anyhow::anyhow!("No data found in JSON"))?
99        .1
100        .len();
101    for (lang, mess) in &data {
102        if mess.len() != count {
103            return Err(anyhow::anyhow!(
104                "Language {lang} Message count mismatch: expected {}, got {}",
105                count,
106                mess.len()
107            ));
108        }
109    }
110    file.write_all(b"CSTL")?;
111    file.write_u32(0)?; // unk
112    let lang_count = data.len();
113    file.write_size(lang_count)?;
114    for lang in data.keys() {
115        let encoded = encode_string(encoding, lang, false)?;
116        file.write_size(encoded.len())?;
117        file.write_all(&encoded)?;
118    }
119    file.write_size(count)?;
120    for i in 0..count {
121        for mess in data.values() {
122            let m = &mess[i];
123            if let Some(name) = &m.name {
124                let encoded_name = encode_string(encoding, name, false)?;
125                file.write_size(encoded_name.len())?;
126                file.write_all(&encoded_name)?;
127            } else {
128                file.write_size(0)?;
129            }
130            let encoded_mes = encode_string(encoding, &m.message, false)?;
131            file.write_size(encoded_mes.len())?;
132            file.write_all(&encoded_mes)?;
133        }
134    }
135    Ok(())
136}
137
138trait CustomFn {
139    fn read_size(&mut self) -> Result<usize>;
140}
141
142impl<T: Read> CustomFn for T {
143    fn read_size(&mut self) -> Result<usize> {
144        let mut size = 0;
145        loop {
146            let len = self.read_u8()?;
147            size += len as usize;
148            if len != 0xFF {
149                break;
150            }
151        }
152        Ok(size)
153    }
154}
155
156trait CustomWriteFn {
157    fn write_size(&mut self, size: usize) -> Result<()>;
158}
159
160impl<T: Write> CustomWriteFn for T {
161    fn write_size(&mut self, mut size: usize) -> Result<()> {
162        loop {
163            let len = if size > 0xFF { 0xFF } else { size as u8 };
164            self.write_u8(len)?;
165            size -= len as usize;
166            if len != 0xFF {
167                break;
168            }
169        }
170        Ok(())
171    }
172}
173
174#[derive(Debug)]
175/// CSTL script.
176pub struct CstlScript {
177    langs: Vec<String>,
178    data: Vec<Vec<Message>>,
179    lang_index: Option<usize>,
180    custom_yaml: bool,
181}
182
183impl CstlScript {
184    /// Creates a new instance of `CstlScript` from a buffer.
185    ///
186    /// * `buf` - The buffer containing the script data.
187    /// * `encoding` - The encoding of the script.
188    /// * `config` - Extra configuration options.
189    pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
190        let mut langs = Vec::new();
191        let mut data = Vec::new();
192        let mut reader = MemReader::new(buf);
193        let mut magic = [0; 4];
194        reader.read_exact(&mut magic)?;
195        if &magic != b"CSTL" {
196            return Err(anyhow::anyhow!("Invalid CSTL magic number"));
197        }
198        let unk = reader.read_u32()?;
199        if unk != 0 {
200            return Err(anyhow::anyhow!("Unknown CSTL unk value: {}", unk));
201        }
202        let lang_count = reader.read_size()?;
203        for _ in 0..lang_count {
204            let len = reader.read_size()?;
205            let s = reader.read_fstring(len, encoding, false)?;
206            langs.push(s);
207            data.push(Vec::new());
208        }
209        let count = reader.read_size()?;
210        let mut i = 0;
211        loop {
212            let name_len = reader.read_size()?;
213            let name = if name_len > 0 {
214                Some(reader.read_fstring(name_len, encoding, false)?)
215            } else {
216                None
217            };
218            let mes_len = reader.read_size()?;
219            let message = reader.read_fstring(mes_len, encoding, false)?;
220            data[i % lang_count].push(Message { name, message });
221            i += 1;
222            if reader.is_eof() {
223                break;
224            }
225        }
226        if i != count * lang_count {
227            return Err(anyhow::anyhow!(
228                "CSTL data count mismatch: expected {}, got {}",
229                i,
230                count * langs.len()
231            ));
232        }
233        for (i, lang) in langs.iter().enumerate() {
234            if data[i].len() != count {
235                return Err(anyhow::anyhow!(
236                    "CSTL language '{}' data count mismatch: expected {}, got {}",
237                    lang,
238                    count,
239                    data[i].len()
240                ));
241            }
242        }
243        let lang_index = config
244            .cat_system_cstl_lang
245            .as_ref()
246            .and_then(|lang| langs.iter().position(|l| l == lang));
247        if config.cat_system_cstl_lang.is_some() && lang_index.is_none() {
248            eprintln!(
249                "Warning: specified language '{}' not found in CSTL script",
250                config.cat_system_cstl_lang.as_ref().unwrap()
251            );
252            crate::COUNTER.inc_warning();
253        }
254        Ok(CstlScript {
255            langs,
256            data,
257            lang_index,
258            custom_yaml: config.custom_yaml,
259        })
260    }
261}
262
263impl Script for CstlScript {
264    fn default_output_script_type(&self) -> OutputScriptType {
265        OutputScriptType::Json
266    }
267
268    fn default_format_type(&self) -> FormatOptions {
269        FormatOptions::None
270    }
271
272    fn is_output_supported(&self, _: OutputScriptType) -> bool {
273        true
274    }
275
276    fn custom_output_extension<'a>(&'a self) -> &'a str {
277        if self.custom_yaml { "yaml" } else { "json" }
278    }
279
280    fn extract_messages(&self) -> Result<Vec<Message>> {
281        if self.langs.is_empty() || self.data.is_empty() {
282            return Err(anyhow::anyhow!("CSTL script has no languages or data"));
283        }
284        Ok(self.data[self.lang_index.unwrap_or(0)]
285            .iter()
286            .map(|m| Message {
287                name: m.name.clone(),
288                message: m.message.replace("\\n", "\n"),
289            })
290            .collect())
291    }
292
293    fn import_messages<'a>(
294        &'a self,
295        messages: Vec<Message>,
296        mut file: Box<dyn WriteSeek + 'a>,
297        _filename: &str,
298        encoding: Encoding,
299        replacement: Option<&'a ReplacementTable>,
300    ) -> Result<()> {
301        let mut data = self.data.clone();
302        let index = self.lang_index.unwrap_or(0);
303        if data[index].len() != messages.len() {
304            return Err(anyhow::anyhow!(
305                "CSTL script language '{}' message count mismatch: expected {}, got {}",
306                self.langs[index],
307                data[index].len(),
308                messages.len()
309            ));
310        }
311        for (i, m) in data[index].iter_mut().enumerate() {
312            if let Some(n) = &mut m.name {
313                let mut name = match &messages[i].name {
314                    Some(name) => name.clone(),
315                    None => return Err(anyhow::anyhow!("Message {i} name is missing.")),
316                };
317                if let Some(replacement) = replacement {
318                    for (k, v) in &replacement.map {
319                        name = name.replace(k, v);
320                    }
321                }
322                *n = name;
323            }
324            let mut mes = messages[i].message.clone();
325            if let Some(replacement) = replacement {
326                for (k, v) in &replacement.map {
327                    mes = mes.replace(k, v);
328                }
329            }
330            m.message = mes.replace("\n", "\\n");
331        }
332        file.write_all(b"CSTL")?;
333        file.write_u32(0)?; // unk
334        let lang_count = self.langs.len();
335        file.write_size(lang_count)?;
336        for lang in &self.langs {
337            let encoded = encode_string(encoding, &lang, false)?;
338            file.write_size(encoded.len())?;
339            file.write_all(&encoded)?;
340        }
341        let count = data[index].len();
342        file.write_size(count)?;
343        for i in 0..count {
344            for j in 0..lang_count {
345                let m = &data[j][i];
346                if let Some(name) = &m.name {
347                    let encoded_name = encode_string(encoding, name, false)?;
348                    file.write_size(encoded_name.len())?;
349                    file.write_all(&encoded_name)?;
350                } else {
351                    file.write_size(0)?;
352                }
353                let encoded_mes = encode_string(encoding, &m.message, false)?;
354                file.write_size(encoded_mes.len())?;
355                file.write_all(&encoded_mes)?;
356            }
357        }
358        Ok(())
359    }
360
361    fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> {
362        let mut d = BTreeMap::new();
363        for (lang, data) in self.langs.iter().zip(&self.data) {
364            d.insert(lang, data);
365        }
366        let s = if self.custom_yaml {
367            serde_yaml_ng::to_string(&d)
368                .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))?
369        } else {
370            serde_json::to_string(&d)
371                .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))?
372        };
373        let s = encode_string(encoding, &s, false)?;
374        let mut file = std::fs::File::create(filename)?;
375        file.write_all(&s)?;
376        Ok(())
377    }
378
379    fn custom_import<'a>(
380        &'a self,
381        custom_filename: &'a str,
382        file: Box<dyn WriteSeek + 'a>,
383        encoding: Encoding,
384        output_encoding: Encoding,
385    ) -> Result<()> {
386        create_file(
387            custom_filename,
388            file,
389            encoding,
390            output_encoding,
391            self.custom_yaml,
392        )
393    }
394}